Fix timers firing up to 1ms early#755
Open
pctablet505 wants to merge 2 commits into
Open
Conversation
libuv resamples its internal clock as the floor of the current monotonic time truncated to whole milliseconds, then schedules a timer for loop->time + timeout. Since the fractional millisecond already elapsed at the moment of sampling is discarded, the timer can fire up to ~1ms before the requested delay has actually passed. This made asyncio.sleep(n) occasionally return slightly under n, as reported in MagicStack#739. Pad the timeout handed to uv_timer_start() by 1ms to compensate, while leaving the reported deadline (get_when()/TimerHandle.when()) as the originally requested value.
pctablet505
marked this pull request as ready for review
July 17, 2026 12:47
transport._wait() only waits for the child to be reaped (SIGCHLD), not for its stdout pipe to be drained and closed. Process exit and pipe EOF reach libuv through independent kernel mechanisms and can be observed in different event loop iterations, so the test could assert on proto.stages before connection_lost had actually fired, sometimes leaving the pipe/process transports alive past the end of the test. Wait on the protocol's own connection_lost instead, which uvloop only fires once the process has exited and all pipes have disconnected.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #739.
libuv's internal clock is refreshed as the floor of the current monotonic
time truncated to whole milliseconds, and
uv_timer_start()schedules thecallback for
loop->time + timeout. Since the fractional millisecond thathad already elapsed at the moment of sampling is discarded, a timer can end
up firing up to ~1ms before the requested delay has actually elapsed. This
is what causes
asyncio.sleep(n)to occasionally return a bit undern,as described in the issue.
The fix pads the timeout passed to
uv_timer_start()by 1ms to compensatefor that truncation, while leaving the reported deadline
(
get_when()/TimerHandle.when()) at the originally requested value.Added a regression test that checks
asyncio.sleep()never returns earlyrelative to a wall-clock (
time.monotonic()) measurement, since theexisting rounding regression test (
test_call_later_rounding, for #233)compares against the loop's own internal clock and therefore can't observe
this particular truncation.